package com.ld.shieldsb.pdf;

import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.util.Map;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.lang.StringEscapeUtils;
import org.jsoup.Jsoup;
import org.w3c.dom.Document;
import org.xhtmlrenderer.pdf.ITextRenderer;
import org.xhtmlrenderer.pdf.PDFEncryption;

import com.itextpdf.text.pdf.PdfWriter;
import com.ld.shieldsb.common.core.model.PropertiesModel;
import com.ld.shieldsb.common.core.util.BeetlUtil;
import com.ld.shieldsb.common.core.util.StringUtils;
import com.ld.shieldsb.pdf.exception.DocumentGeneratingException;
import com.ld.shieldsb.pdf.factory.ITextRendererObjectFactory;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class PdfDocumentGenerator {

    /**
     * 使用模板,模板数据,生成pdf
     * 
     * @Title: generate
     * @Description: 使用模板,模板数据,生成pdf
     * @param template
     *            classpath中路径模板路径
     * @param documentVo
     *            模板数据
     * @param outputFile
     *            生成pdf的路径
     * @author lihengjun 修改时间： 2013年11月5日 下午1:38:53 修改内容：新建
     * @throws DocumentGeneratingException
     */
    public boolean generate(String template, Map<String, Object> variables, String outputFile) throws DocumentGeneratingException {
        Config config = new Config();
        return generate(template, variables, outputFile, config);
    }

    public boolean generate(String template, Map<String, Object> variables, String outputFile, Config config)
            throws DocumentGeneratingException {
        try {
            String htmlContent = getHtmlContent(template, variables, config);
            // htmlContent = StringEscapeUtils.escapeXml(htmlContent);
            this.generate(htmlContent, outputFile, config);

            log.info("The variables [map:" + variables + "] is generated successfully,and stored in [" + outputFile + "]");
        } catch (Exception e) {
            String error = "The variables [map:" + variables + "] is failed to generate";
            log.error(error, e);
            throw new DocumentGeneratingException(error, e);
        }

        return true;
    }

    /**
     * 调试时输出内容的html文件
     * 
     * @Title outHtmlWithDebug
     * @author 吕凯
     * @date 2019年10月16日 上午9:55:58
     * @param htmlContent
     * @throws FileNotFoundException
     * @throws UnsupportedEncodingException
     * @throws IOException
     *             void
     */
    private void outHtmlWithDebug(String htmlContent) throws FileNotFoundException, UnsupportedEncodingException, IOException {
        boolean outPdfFile = PropertiesModel.CONFIG.getBoolean("outPdfFile", false);
        if (outPdfFile) {
            String filePath = PropertiesModel.CONFIG.getString("outPdfFilePath", "out.html");
            outHtml2File(htmlContent, filePath); // 输出文件调试用
        }
    }

    /**
     * 输出html到文件,用于调试
     * 
     * @Title outHtml2File
     * @author 吕凯
     * @date 2016年11月30日 上午11:54:23
     * @param htmlContent
     * @throws FileNotFoundException
     * @throws UnsupportedEncodingException
     * @throws IOException
     *             void
     */
    private void outHtml2File(String htmlContent, String filePath) throws FileNotFoundException, UnsupportedEncodingException, IOException {
        File outFile = new File(filePath);
        if (outFile.getParentFile() == null) {
            outFile = new File(outFile.getAbsolutePath().replace("\\", "/"));
        }
        log.warn("outfile=====" + outFile.getAbsolutePath());
        if (outFile.getParentFile() != null && !outFile.getParentFile().exists()) {
            outFile.getParentFile().mkdirs();
        }
        try (OutputStream os = new FileOutputStream(outFile);
                BufferedWriter rfw = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));) {
            rfw.write(htmlContent);
            rfw.flush();
        }

    }

    /**
     * 
     * 将内容到指定的文件
     * 
     * @Title generate
     * @author 吕凯
     * @date 2019年10月16日 上午9:53:10
     * @param htmlContent
     * @param outputFile
     *            文件完整路径
     * @throws Exception
     *             void
     */
    @SuppressWarnings("unchecked")
    public void generate(String htmlContent, String outputFile, Config config) throws Exception {
        htmlContent = parseHtml(htmlContent, config);
        outHtmlWithDebug(htmlContent);
        ITextRenderer iTextRenderer = null;
        try {
            // DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            // Document doc = builder.parse(new ByteArrayInputStream(htmlContent.getBytes("UTF-8")));
            File f = new File(outputFile);
            if (f != null && !f.getParentFile().exists()) {
                f.getParentFile().mkdir();
            }
            iTextRenderer = (ITextRenderer) ITextRendererObjectFactory.getObjectPool().borrowObject(); // 获取对象池中对象

            try (OutputStream out = new FileOutputStream(outputFile)) {
                iTextRenderer.setDocumentFromString(htmlContent);
                // iTextRenderer.setDocument(doc, null);
                iTextRenderer.layout();
                if (config.isEncrypt()) {
                    iTextRenderer.setPDFEncryption(getEncryption(config)); // 加密
                } else {
                    iTextRenderer.setPDFEncryption(null);
                }
                iTextRenderer.createPDF(out);
            } catch (Exception e) {
                ITextRendererObjectFactory.getObjectPool().invalidateObject(iTextRenderer);
                iTextRenderer = null;
                throw e;
            }
        } catch (Exception e) {
            throw e;
        } finally {

            if (iTextRenderer != null) {
                try {
                    ITextRendererObjectFactory.getObjectPool().returnObject(iTextRenderer);
                } catch (Exception ex) {
                    log.error("Cannot return object from pool.", ex);
                }
            }
        }
    }

    /**
     * 输出到流
     * 
     * @Title generate
     * @author 吕凯
     * @date 2017年11月20日 上午8:34:56
     * @param htmlContent
     * @param out
     * @throws Exception
     *             void
     */
    @SuppressWarnings("unchecked")
    public void generate(String htmlContent, OutputStream out, Config config) throws Exception {
        htmlContent = parseHtml(htmlContent, config);
        outHtmlWithDebug(htmlContent);
        ITextRenderer iTextRenderer = null;
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); // 防止XXE攻击
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document doc = builder.parse(new ByteArrayInputStream(htmlContent.getBytes("UTF-8")));

            iTextRenderer = (ITextRenderer) ITextRendererObjectFactory.getObjectPool().borrowObject(); // 获取对象池中对象

            try {
                iTextRenderer.setDocument(doc, null);
                iTextRenderer.layout();
                if (config.isEncrypt()) {
                    iTextRenderer.setPDFEncryption(getEncryption(config)); // 加密
                } else {
                    iTextRenderer.setPDFEncryption(null);
                }
                iTextRenderer.createPDF(out);
            } catch (Exception e) {
                ITextRendererObjectFactory.getObjectPool().invalidateObject(iTextRenderer);
                iTextRenderer = null;
                throw e;
            }

        } catch (Exception e) {
            throw e;
        } finally {
            if (iTextRenderer != null) {
                try {
                    ITextRendererObjectFactory.getObjectPool().returnObject(iTextRenderer);
                } catch (Exception ex) {
                    log.error("Cannot return object from pool.", ex);
                }
            }
        }
    }

    /**
     * 加密
     * 
     * @Title getEncryption
     * @author 吕凯
     * @date 2017年10月24日 上午10:15:55
     * @return PDFEncryption
     */
    private PDFEncryption getEncryption(Config config) {
        // 第一个密码是用户密码，输入该密码打开pdf后根据设置的权限进行控制，第二个密码属于所有者密码，使用该密码打开pdf权限不受控制
        // int permissions = PdfWriter.ALLOW_COPY|PdfWriter.ALLOW_MODIFY_CONTENTS|PdfWriter.ALLOW_PRINTING;
        int permissions = PdfWriter.ALLOW_SCREENREADERS | PdfWriter.ALLOW_PRINTING;
        PDFEncryption encrypt = null;
        if (StringUtils.isNotEmpty(config.getEncryptPwd())) {
            encrypt = new PDFEncryption(null, config.getEncryptPwd().getBytes(), permissions);
        }
        return encrypt;
    }

    /**
     * 根据模板生成内容到指定的流
     * 
     * @Title generate
     * @author 吕凯
     * @date 2019年10月16日 上午9:49:01
     * @param template
     *            模板路径，相对于class目录的templates目录的相对路径
     * @param variables
     *            模板变量值
     * @param out
     *            输出流
     * @return
     * @throws DocumentGeneratingException
     *             boolean
     */
    public boolean generate(String template, Map<String, Object> variables, OutputStream out) throws DocumentGeneratingException {
        Config config = new Config();
        return generate(template, variables, out, config);
    }

    /**
     * 
     * 根据模板生成内容到指定的流
     * 
     * @Title generate
     * @author 吕凯
     * @date 2019年10月16日 上午10:07:22
     * @param template
     *            模板路径，相对于class目录的templates目录的相对路径
     * @param variables
     *            模板变量值
     * @param out
     *            输出流
     * @param config
     *            配置参数，指定是否加密，是否校验html标签等
     * @return
     * @throws DocumentGeneratingException
     *             boolean
     */
    public boolean generate(String template, Map<String, Object> variables, OutputStream out, Config config)
            throws DocumentGeneratingException {
        try {
            String htmlContent = getHtmlContent(template, variables, config);
            this.generate(htmlContent, out, config);

            log.info("The variables [map:" + variables + "] is generated successfully]");
        } catch (Exception e) {
            String error = "The variables [map:" + variables + "] is failed to generate";
            log.error(error, e);
            throw new DocumentGeneratingException(error, e);
        }

        return true;
    }

    /**
     * 获取原始的html文本
     * 
     * @Title getHtmlContent
     * @author 吕凯
     * @date 2017年12月27日 上午8:54:40
     * @param template
     * @param variables
     * @return
     * @throws IOException
     *             String
     */
    private String getHtmlContent(String template, Map<String, Object> variables, Config config) throws IOException {
        String htmlContent = BeetlUtil.render(template, variables);
        return htmlContent;
    }

    /**
     * 对html文本进行处理，并返回处理后的结果
     * 
     * @Title parseHtml
     * @author 吕凯
     * @date 2019年10月17日 上午10:00:38
     * @param htmlContent
     * @param config
     * @return
     * @throws IOException
     *             String
     */
    private String parseHtml(String htmlContent, Config config) throws IOException {
        if (config.isCheckHtml()) {
            org.jsoup.nodes.Document doc = Jsoup.parse(htmlContent); // jsoup补全，防止有未闭合的标签，但是jsoup与flying-saucer的校验可能有冲突的地方，严格来说jsoup更标准
            htmlContent = doc.toString();
            // 因为flying-saucer要求严格闭合，而jsoup处理时可能把一部分标签的闭合去掉了，所以需要手动处理
            htmlContent = htmlContent.replaceAll("(<hr .+?)>", "$1/>").replaceAll("<hr>", "<hr />").replaceAll("<br>", "<br />")
                    .replaceAll("(<img .+?)>", "$1/>").replaceAll("(<col .+?)>", "$1/>").replaceAll("<col>", "<col />"); // jsoup1.11自动把这些元素后面的/去掉，需要手动重新加上
        }

        htmlContent = htmlContent.replaceAll("&lt;", "<![CDATA[<]]>").replaceAll("&gt;", "<![CDATA[>]]>"); // jsoup处理的不闭合的小于号大于号
                                                                                                           // <![CDATA[ ]]>保留原文本
        htmlContent = StringEscapeUtils.unescapeHtml(htmlContent); // &middot;(.)经过jsoup后会被还原为&middot;，所以unescapeHtml要在其后，存在<>小于号大于号时有问题
        /*
         * 去除官方定义了XML的无效字符分为三段： 0x00 - 0x08, 0x0b - 0x0c, 0x0e - 0x1f
         */
        htmlContent = htmlContent.replaceAll("[\\x00-\\x08\\x0b-\\x0c\\x0e-\\x1f]", "");
        // htmlContent = htmlContent.replaceAll("&nbsp;", "");
        htmlContent = htmlContent.replaceAll("&", "&amp;"); // xml中特殊字符需要转义
        // htmlContent = StringEscapeUtils.escapeXml(htmlContent);
        return htmlContent;
    }

    public static void main(String[] args) {
        String str = "&middot;·123&lt;<123&amp;00&";
        org.jsoup.nodes.Document doc = Jsoup.parse(str); // jsoup补全
        String htmlContent = doc.toString();
        System.out.println(str + "》" + htmlContent);
        String ss1 = StringEscapeUtils.unescapeHtml(str);
        System.out.println(str + "》" + ss1);
        String ss = StringEscapeUtils.escapeXml(str);
        // ss = StringEscapeUtils.unescapeXml(ss);
        System.out.println(ss + "》" + ss1);
    }
}
